package com.androwit.engine;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.apache.commons.beanutils.BeanUtils;

import com.androwit.engine.mapping.Converter;
import com.androwit.engine.mapping.MappingField;
import com.androwit.engine.mapping.MappingFieldsDefiner;
import com.androwit.engine.mapping.OneSensValueConverter;

/**
 * Parses data from its target format to its java equivalent.
 * 
 * The data parsing is done through {@link MappingReader} while the metadata of
 * each row is in {@link MappingFieldsDefiner}
 * 
 * <p>
 * The algorithm is the following for each row in the cursor: <br/>
 * {@link MappingReader#getRelevantValues()} returns the relevant values to
 * identify how to read the currently pointed cursor: since the library is
 * designed to handle heterogeneous data, each row can have its own column set
 * relevant to identify the nature of the underlying structure: this can be a
 * special column (like type) or whatever you decide as relevant for your data.
 * <br/>
 * The method {@link MappingFieldsDefiner#createInstance(Map)} uses the return
 * value of the method above to create the right instance that will contain the
 * data. <br/>
 * 
 * The method {@link MappingFieldsDefiner#getMappedFields(Map)} is then
 * responsible to inform about the fields that should be read to fill the newly
 * created instance (notice it is not mandatory to create a new instance: it is
 * perfectly legal to use the same one if you need it. In that case you will
 * need to be notified when all fields have been read: you can do that by
 * overriding {@link #parseLine(MappingFieldsDefiner, MappingReader)} and adding
 * your code after the call to super)
 * 
 * <br/>
 * 
 * Each field is read through the method {@link #get(MappingField)} and used to
 * fill each property of the current instance. The {@link MappingField} contains
 * a {@link Converter} that will allow the transforming of the target (typically
 * String format) data to its Java representing instance <br/>
 * <br/>
 * 
 * You can have special 'magic' fields: each field that returns true in
 * {@link MappingField#handledSpecifically()} will be responsible to insert the
 * data to the current instance itself through a special converter that must
 * implement {@link OneSensValueConverter}. Don't forget to handle the field
 * specifically in your implementation of
 * {@link MappingReader#get(MappingField)} to return the required information
 * for your converter to work properly
 * 
 * @author Zied Hamdi Founder of http://1vu.fr
 */
public class MappingParser<J, K, V> {
	protected static final char DEFAULT_DELIMITER = ';';
	protected int skipItems = 0;

	public MappingParser() {
	}

	public MappingParser(int skipItems) {
		this.skipItems = skipItems;
	}

	public List<? extends Object> fromTarget(
			MappingFieldsDefiner<J, K, V> fieldsDefiner, MappingReader<K, V> reader)
			throws Exception {
		List<Object> toReturn = new ArrayList<Object>();
		skipItems(reader);
		while (reader.readRecord()) {
			Object instance = parseLine(fieldsDefiner, reader);
			toReturn.add(instance);
		}
		return toReturn;
	}

	public Object parseLine(MappingFieldsDefiner<J, K, V> fieldsDefiner,
			MappingReader<K, V> reader) throws Exception {
		Map<K, V> values = reader.getRelevantValues(fieldsDefiner);
		Object toReturn = fieldsDefiner.createInstance(values);
		if (toReturn == null)
			throw new UnsupportedOperationException("No instance to read \n\t"
					+ values);

		MappingField<J, K, V>[] fields = fieldsDefiner.getMappedFields(values);
		if (fields.length == 0)
			return null;

		for (int i = 0; i < fields.length; i++) {
			MappingField<J, K, V> field = fields[i];
			doSetFieldInJava(field, reader, toReturn);
		}

		return toReturn;
	}

	/**
	 * Sets the value of the field <code>field</code> in target
	 * <code>toReturn</code>
	 * 
	 * @param field
	 *          field definition
	 * @param reader
	 *          the value reader from source
	 * @param toReturn
	 *          the java instance that will host the converted value of field
	 */
	protected void doSetFieldInJava(MappingField<J, K, V> field,
			MappingReader<K, V> reader, Object toReturn) {
		Converter<J, V> converter = field.getConverter();
		V value = reader.get(field);
		try {
			if (field.handledSpecifically()) {
				if (converter != null) {
					// for better performance results, we don't do an instanceof test
					// here, we
					// rather catch a ClassCastException
					try {
						((OneSensValueConverter<J, V>) converter).setFromTarget(value,
								toReturn, field);
						return;
					} catch (ClassCastException ex) {
						throw new IllegalStateException(
								"It is not allowed to have a handledSpecifically() MappingField with a converter of type "
										+ converter.getClass()
										+ " you either must return false in handledSpecifically() or the converter must implement "
										+ OneSensValueConverter.class.getSimpleName());
					}
				} else
					throw new IllegalStateException(
							"It is not allowed to have a handledSpecifically() MappingField with a null converter. You should either return false in handledSpecifically() or return a non null converter in getConverter() for MappingField instance "
									+ field);
			}

			Object convertedValue = value;
			if (converter != null)
				convertedValue = converter.fromConverted(value);
			// if converter is null we assume the value is of the same type
			// in both environements

			BeanUtils.setProperty(toReturn, field.getName(), convertedValue);

		} catch (Throwable ex) {
			throw new RuntimeException("Error on field :'" + field + "' of type : "
					+ toReturn.getClass().getSimpleName() + " on key : '"
					+ field.getKey() + "' with value: '" + value + "' (of class:"
					+ (value != null ? value.getClass().getSimpleName() : "null")
					+ ") converted by: " + converter + "\n", ex);
		}
	}

	private void skipItems(MappingReader<K, V> reader) throws IOException {
		for (int i = 0; i < skipItems; i++) {
			reader.readRecord();
		}
	}
}
